#pragma once

#include <arpa/inet.h>
#include <netinet/ip6.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iterator>
#include <optional>
#include <string>

#define BYTES_MALLOC malloc
#define BYTES_FREE free
#define BYTES_REALLOC realloc

namespace bio {

enum class LogLevel : unsigned int {
    EMERG = 0,
    ALERT = 1,
    CRIT = 2,
    ERR = 3,
    WARN = 4,
    NOTICE = 5,
    INFO = 6,
    DEBUG = 7
};

const char* LogLevelStr[] = {"EMERG ", "ALERT ", "CRIT  ", "ERR   ",
                             "WARN  ", "NOTICE", "INFO  ", "DEBUG "};

template <LogLevel level = LogLevel::INFO>
void log(const char* what) {
    printf("[%s] %s\n", LogLevelStr[(size_t)level], what);
}

class errno_exception : public std::runtime_error {
   private:
    error_t err;

   public:
    errno_exception() : runtime_error(strerror(errno)), err(errno) {}
    errno_exception(error_t err) : runtime_error(strerror(errno)), err(err) {}

    error_t get_errno() { return err; }
};

std::optional<struct in6_addr> addr_parse(std::string& str) {
    struct in6_addr addr;
    if (inet_pton(AF_INET6, str.c_str(), &addr) != 1) {
        return std::nullopt;
    }
    return addr;
}

std::optional<struct in6_addr> addr_parse(std::string&& str) {
    return addr_parse(static_cast<std::string&>(str));
}

std::optional<struct in6_addr> addr4_parse(std::string& str) { return addr_parse("::FFFF:" + str); }

std::string addr_to_string(struct in6_addr& addr) {
    char buf[INET6_ADDRSTRLEN];
    assert(inet_ntop(AF_INET6, &addr, buf, sizeof(buf)) != nullptr);
    return std::string(buf);
}

std::string sockaddr_to_string(struct sockaddr_in6& addr) {
    return addr_to_string(addr.sin6_addr) + "%" + std::to_string(ntohs(addr.sin6_scope_id)) + ":" +
                          std::to_string(ntohs(addr.sin6_port));
}

class bytes {
   private:
    void copyfrom(const bytes& other) {
        if (len <= sizeof(b_data.buf)) {
            std::copy(other.b_data.buf, other.b_data.buf + sizeof(other.b_data.buf), b_data.buf);
        } else {
            b_data.ptr = reinterpret_cast<uint8_t*>(BYTES_MALLOC(len));
            std::copy(other.b_data.ptr, other.b_data.ptr + len, b_data.ptr);
            b_data.actual = other.b_data.actual;
        }
    }
    void movefrom(const bytes& other) {
        if (len <= sizeof(b_data.buf)) {
            std::copy(other.b_data.buf, other.b_data.buf + sizeof(other.b_data.buf), b_data.buf);
        } else {
            b_data.ptr = other.b_data.ptr;
            b_data.actual = other.b_data.actual;
        }
    }

   public:
    bytes() : len(0) {}

    bytes(const bytes& other) : len(other.len) { copyfrom(other); }

    bytes& operator=(const bytes& other) {
        len = other.len;
        copyfrom(other);
        return *this;
    }

    bytes(bytes&& other) : len(other.len) {
        movefrom(other);
        other.len = 0;
        other.b_data.actual = 0;
    }

    bytes& operator=(bytes&& other) {
        len = other.len;
        movefrom(other);
        other.len = 0;
        other.b_data.actual = 0;
        return *this;
    }

    bytes(size_t size) : len(size) {
        if (len > sizeof(b_data.buf)) {
            b_data.ptr = reinterpret_cast<uint8_t*>(BYTES_MALLOC(len));
            b_data.actual = len;
        }
    }

    bytes(const char* str) : len(strlen(str) + 1) {
        if (len > sizeof(b_data.buf)) {
            b_data.ptr = reinterpret_cast<uint8_t*>(BYTES_MALLOC(len));
            b_data.actual = len;
        }
        std::copy(str, str + len, data());
    }

    ~bytes() {
        if (len > sizeof(b_data.buf)) {
            BYTES_FREE(b_data.ptr);
        }
    }

    const uint8_t* data() const {
        if (len <= sizeof(b_data.buf)) {
            return b_data.buf;
        } else {
            return b_data.ptr;
        }
    }

    uint8_t* data() {
        if (len <= sizeof(b_data.buf)) {
            return b_data.buf;
        } else {
            return b_data.ptr;
        }
    }

    size_t size() const { return len; }

    void resize(size_t new_len) {
        // CASES
        // Same length: do nothing
        if (new_len == len) {
            return;
            // zero length: free heap buffer, if any
        } else if (new_len == 0) {
            if (len >= sizeof(b_data.buf)) {
                BYTES_FREE(b_data.ptr);
            }
            // Moving from heap to stack
        } else if (new_len <= sizeof(b_data.buf) && len > sizeof(b_data.buf)) {
            uint8_t* heap = b_data.ptr;
            std::copy(heap, heap + new_len, b_data.buf);
            BYTES_FREE(heap);
            // Moving from stack to heap
        } else if (new_len > sizeof(b_data.buf) && len <= sizeof(b_data.buf)) {
            uint8_t* heap = reinterpret_cast<uint8_t*>(BYTES_MALLOC(new_len));
            std::copy(b_data.buf, b_data.buf + len, heap);
            b_data.ptr = heap;
            b_data.actual = new_len;
            // Increasing heap size, if there isn't enough actual space
        } else if (len > sizeof(b_data.buf) && new_len > len) {
            if (new_len > b_data.actual) {
                b_data.ptr = reinterpret_cast<uint8_t*>(BYTES_REALLOC(b_data.ptr, new_len));
                b_data.actual = new_len;
            }
            // Decreasing heap size, but only actually decreasing if it's significantly smaller
        } else if (len > sizeof(b_data.buf) && new_len < len / 2) {
            b_data.ptr = reinterpret_cast<uint8_t*>(BYTES_REALLOC(b_data.ptr, new_len));
            b_data.actual = new_len;
        }

        len = new_len;
    }

    // I swear this is null-terminated
    std::string to_string() {
        assert(data()[len - 1] == '\0');
        return std::string(reinterpret_cast<char*>(data()));
    }

    // Terminate for me
    std::string to_string_checked() {
        if (data()[len - 1] == '\0') {
            return to_string();
        }
        std::string str;
        str.resize(len + 1);
        std::copy(data(), data() + len, const_cast<char*>(str.c_str()));
        str[len] = '\0';
        return str;
    }

    bytes operator+(const bytes& other) {
        bytes b{len + other.len};
        std::copy(data(), data() + len, b.data());
        std::copy(other.data(), other.data() + other.len, b.data() + len);
        return b;
    }

    bytes& operator+=(const bytes& other) {
        size_t old_len = len;

        resize(len + other.len);

        std::copy(other.data(), other.data() + other.len, data() + old_len);

        return *this;
    }

    int operator<=>(const bytes& other) {
        int res = memcmp(data(), other.data(), std::min(len, other.len));
        if (res == 0) {
            if (len > other.len) {
                return 1;
            } else if (len < other.len) {
                return -1;
            }
        }
        return res;
    }

#define BYTES_OPERATOR(op) \
    bool operator op(const bytes& other) { return (this->operator<=>(other))op 0; }
    BYTES_OPERATOR(==);
    BYTES_OPERATOR(!=);
    BYTES_OPERATOR(<);
    BYTES_OPERATOR(<=);
    BYTES_OPERATOR(>);
    BYTES_OPERATOR(>=);
#undef BYTES_OPERATOR

   private:
    union {
        uint8_t buf[64];
        struct {
            uint8_t* ptr;
            size_t actual;
        };
    } b_data;
    size_t len;
};
}  // namespace bio

std::ostream& operator<<(std::ostream& output, const bio::bytes& bytes) {
    char buf[bytes.size() * 3 + 1];
    for (size_t i = 0; i < bytes.size(); i++) {
        snprintf(&buf[i * 3], 4, "%02x ", bytes.data()[i]);
    }
    output << buf;
    return output;
}
